package edu.kit.ksri.db.mturk.plugins.correctness;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.lang.String;
import org.apache.log4j.Logger;

import com.amazonaws.mturk.requester.Assignment;

import edu.kit.ksri.db.mturk.connectionhandlers.HITManager;
import edu.kit.ksri.db.mturk.connectionhandlers.SimHITManager;
import edu.kit.ksri.db.mturk.plugins.correctness.answers.AbstractAssignmentAnswer;
import edu.kit.ksri.db.mturk.plugins.correctness.answers.AssignmentAnswerFactory;
import edu.kit.ksri.db.mturk.plugins.correctness.answers.BasicAssignmentAnswer;
import edu.kit.ksri.db.mturk.qualifications.QualificationManager;
import edu.kit.ksri.db.mturk.utils.messaging.Result;
import edu.kit.ksri.db.mturk.utils.messaging.ResultOutputCollector;
import edu.kit.ksri.db.mturk.var.ConnectionMode;
import edu.kit.ksri.db.mturk.var.StaticInformation;
import edu.kit.ksri.db.mturk.workerpools.AbstractWorker;
import edu.kit.ksri.db.mturk.workerpools.CSP1Capable;
import edu.kit.ksri.db.mturk.workerpools.PoolManager;
import edu.kit.ksri.db.mturk.workerpools.SimWorker;
import edu.kit.ksri.db.mturk.workerpools.StandardWorker;

/**
 * Copyright (c) 2010-2011 Karlsruhe Institute of Technology (KIT), Germany
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 * 
 * --------------------------------------------------------------------------
 * 
 * Software: CSP/WMV tool for dynamic quality management of microtasks
 * http://www.ksri.kit.edu/Upload/Users/PeopleClouds/HCOMP2011_An_extendable_toolkit_for_managing_quality_of_human-based_eServices.pdf
 * 
 * Organization: Karlsruhe Institute of Technology (KIT), Germany
 * http://www.kit.edu
 * 
 * Project: PeopleClouds project of the Karlsruhe Service Research Institute(KSRI)
 * http://www.ksri.kit.edu/Default.aspx?PageId=818
 * 
 * Contributing authors: David Bermbach, Sandra Rath, Pascal Wichmann, Robert Kern
 */
public class WMVBinomial extends AbstractCSP1AndWMVImplementation {

	private static Logger basiclog = Logger.getLogger("basic." + WMVBinomial.class);

	private static final Logger qualilog = Logger.getLogger("quali." 
			+ WMVBinomial.class);

	// calculate factorial of n
	public static long factorial (int n)
	{
	  return n == 0 ? 1 : n * factorial (n-1);
	}
	
	@Override
	protected void initWMV(final String hitID) {
		Result r = ResultOutputCollector.getInstance().get(hitID);
		basiclog.info("Initializing WMV for HIT " + hitID + " with "
				+ r.getAssignments().size() + " assignments.");

		AbstractWorker[] workers = null;
		//if (StaticInformation.scenario.getPrefix().equals("AddressResearch"))
		if (StaticInformation.scenario.getConnectionMode().equals(ConnectionMode.SIM))
			workers = new SimWorker[r.getAssignments().size()];
		else 
			workers = new StandardWorker[r.getAssignments().size()];
		

		int counter = 0;
		for (Assignment a : r.getAssignments()) {
			workers[counter] = PoolManager.getInstance().getPoolForName(
					"ACTIVE").getWorkerForID(a.getWorkerId());
			if (workers[counter] == null) {
				// must be in pending or blocked pool now
				workers[counter] = PoolManager.getInstance().getPoolForName(
						"PENDING").getWorkerForID(a.getWorkerId());
				if (workers[counter] == null) {
					// must be in blocked pool now
					workers[counter] = PoolManager.getInstance()
							.getPoolForName("BLOCKED").getWorkerForID(
									a.getWorkerId());
					if (workers[counter] == null) {
						// what shall we do if we cannot find our work?
						// disregard the result?
						// FIXME
					}
				}
			}
			counter++;
		}
		// now we got references to all workers which have worked on the
		// specified hit so far as well as their results
		// get parameters for calculation
		double phiMin = StaticInformation.scenario.getMinimumInspectionQualityLevel();
		double epsilonMax = StaticInformation.scenario.getEscalationLimit();
		int minimumNumberOfCategories = StaticInformation.scenario.getMinimumNumberOfCategories();
		double[] correctness = new double[workers.length];
		double[] errorrate = new double[workers.length];
		double[] weight = new double[workers.length];
		double errorWeight = 0;
		
		counter = 0;
		for (AbstractWorker w : workers) {
			correctness[counter] = w.getCorrectnessLevel() / 100.0;
			errorrate[counter] = 1 - correctness[counter];
			weight[counter] = 0;
			counter++;
		}
		AbstractAssignmentAnswer[] answers = new AbstractAssignmentAnswer[workers.length];
		// distinguish between implementing classes for efficiency reasons
		counter = 0;
		if (StaticInformation.scenario.getAnswerComparisonClass()
				.equals(BasicAssignmentAnswer.class)) {
			// is BasicAssignmentAnswer.class
			for (Assignment a : r.getAssignments()) {
				answers[counter++] = AssignmentAnswerFactory
						.createBasicAssignmentAnswerInstance(a.getAnswer());
			}
		} else {
			// is not BasicAssignmentAnswer.class
			for (Assignment a : r.getAssignments()) {
				answers[counter++] = AssignmentAnswerFactory.createInstance(a
						.getAnswer());
			}
		}
		// identify the set of answers (without duplicates)
		Set<AbstractAssignmentAnswer> distinctAnswers = new HashSet<AbstractAssignmentAnswer>();
		for (AbstractAssignmentAnswer aaa : answers)
			distinctAnswers.add(aaa);
		// convert to area to have a fixed order
		AbstractAssignmentAnswer[] distinctAnswersArray = distinctAnswers
				.toArray(new AbstractAssignmentAnswer[0]);
		// Calculate actual number of categories
		// Use number of distinct answers if it exceeds the minimum number of categories
		int categories;
		if (minimumNumberOfCategories >= distinctAnswersArray.length)
			categories = minimumNumberOfCategories;
		else
			categories = distinctAnswersArray.length;
		// calculate for every single result the product of a worker's
		// correctness value if he is favor of this answer or his error rate
		// else. This is equal to the numerator value for each result. The
		// denominator for the final phi_y is then the sum of all these values
		// plus the product of all worker's error rates (the last term to
		// include the probability that all solutions are wrong).
		// the order of the value in correctnessErrorProducts is the same as
		// returned by distinctAnswersArray.
		double[] correctnessErrorProducts = new double[distinctAnswersArray.length];
		String[] correctnessProfiles = new String[distinctAnswersArray.length];
		for (int i = 0; i < distinctAnswersArray.length; i++) {
			int numberOfWorkersDeliveringWrongResults = 0;
			correctnessErrorProducts[i] = 1;
			correctnessProfiles[i] = "";
			for (int j = 0; j < answers.length; j++) {
				if (distinctAnswersArray[i].equals(answers[j])) {
					correctnessErrorProducts[i] *= correctness[j];
					correctnessProfiles[i] = correctnessProfiles[i] + "C";
				}
				else {
					correctnessErrorProducts[i] *= errorrate[j];
					numberOfWorkersDeliveringWrongResults++;
					correctnessProfiles[i] = correctnessProfiles[i] + "F";
				}
			}
			// Calculate weights			
			weight[i] = 1/(categories * Math.pow(categories-1,numberOfWorkersDeliveringWrongResults));			
		}
		// calculate prob of correctness profile with all responses incorrect 
		double errorsProduct = 1;
		for (int j = 0; j < answers.length; j++){
			errorsProduct *= errorrate[j];
			qualilog.info("Error rate of worker " + j + " : " + errorrate[j] );			
		}
		
		// Calculate weight for last term (all answers are assumed to be incorrect)
		// One additional result is being assumed to be incorrect compared to the other weights
		if (categories > answers.length) // more result categories than workers
			errorWeight = 1/Math.pow(categories,answers.length);
		else {
			if (categories == distinctAnswersArray.length) {
				// all result categories are used in the response tuple,
				// therefore not all responses can be incorrect
				errorWeight = 0;
			} else {
				// Calculate stirling term
				double stirlingTerm = 0;
				for (int i=0; i < categories; i++) {
					stirlingTerm = stirlingTerm
					+ (factorial(categories) / ( factorial(categories - i) * factorial(i) ) )
					* Math.pow(categories - i, answers.length)
					* Math.pow(-1, i);
				}
				qualilog.info("Stirling term: " + stirlingTerm );
				errorWeight = 1 / ( Math.pow(categories,answers.length) - stirlingTerm );				
			}
		}
		// calculate the denominator
		double denominator = errorsProduct * errorWeight;
		counter = 0;
		for (double d : correctnessErrorProducts) {
			denominator += weight[counter] * d;
			counter++;
		}
		basiclog.info("P(R) = " + denominator);

		// now calculate the phi_y
		double[] phiYs = new double[distinctAnswersArray.length];
		for (int i = 0; i < phiYs.length; i++) {
			phiYs[i] = correctnessErrorProducts[i] * weight[i] / denominator;
			qualilog.info("Corr profile: " + correctnessProfiles[i]
			    + ", Prob: " + correctnessErrorProducts[i]
                + ", Weight: " + weight[i]
                + ", Phi: " + phiYs[i] );
		}
		qualilog.info("Correctness profile: <all incorrect>"
			    + ", Prob: " + errorsProduct
				+ ", Weight: " + errorWeight );

		// find maximum value and position
		double max = phiYs[0];
		int pos = 0;
		for (int i = 0; i < phiYs.length; i++) {
			if (phiYs[i] > max) {
				max = phiYs[i];
				pos = i;
			}
		}
		qualilog.info("Highest Phi: " + max + ", corr profile: "
				+ correctnessProfiles[pos] );
		String temp = "Got assignments from: ";
		for (AbstractWorker w : workers)
			temp += w.getWorkerId() + ", ";
		basiclog.info(temp);
		
		List<Assignment> assignments = r.getAssignments();
		String answersOutput = "";
		for(Assignment a : assignments){
			answersOutput = answersOutput + AbstractAssignmentAnswer.parseAnswer(a.getAnswer()) + ", ";
		}
		basiclog.info("Answers were: " + answersOutput);
		
		// set currentPhi
		r.setCurrentPhi(phiYs[pos]);
		
		
		// found the best solution so far, compare with phiMin
		if (phiYs[pos] >= phiMin) {
			// is accepted => return and finalize result
			r.setCorrectSolution(distinctAnswersArray[pos]);
			r.setPhiCorrect(phiYs[pos]);
			r.setFinalResult(true);
			ResultOutputCollector.getInstance().put(hitID, r);
			AbstractAssignmentAnswer correct = distinctAnswersArray[pos];
			basiclog.info("RETURNING RESULT FOR " + hitID + "\n" + 
					". Correct answer is " + AbstractAssignmentAnswer.parseAnswer(correct.getRawAnswer()) +
					".\n");
			// update worker statistics
			for (int i = 0; i < workers.length; i++) {
				((CSP1Capable)workers[i]).addAssignmentForBinomialCSP1(true, answers[i]
						.equals(correct));
			}
			return;
		}
		// result cannot be accepted yet, check whether it makes sense to extend
		// it or whether we should rather escalate it back to the requester.
		// since epsilon_y equals the probability P(C) of the correctness profile at position 
		// that belongs to the most probable response

		basiclog.info("Epsilon = " + correctnessErrorProducts[pos]);
		
		if ( correctnessErrorProducts[pos] > epsilonMax) {
			
			// publish one more task, i.e. extend hit
			basiclog.info("EXTENDING " + hitID + "\n");

			boolean success = false;
			if (StaticInformation.scenario.getPrefix() == "AddressResearch" && StaticInformation.scenario.getConnectionMode() == ConnectionMode.SIM){
				success = ((SimHITManager)(HITManager.getInstance())).extendHit(r);
			}else{
				success = HITManager.getInstance().extendHit(hitID);
			}
			
			ResultOutputCollector.getInstance().put(hitID, r);
			int retryIn = 0;
			while (!success) {
				retryIn++;
				// retry
				if (StaticInformation.scenario.getPrefix() == "AddressResearch" && StaticInformation.scenario.getConnectionMode() == ConnectionMode.SIM){
					success = ((SimHITManager)(HITManager.getInstance())).extendHit(r);
				}else{
					success = HITManager.getInstance().extendHit(hitID);
				}
				if (success) {
					basiclog.info("HIT " + hitID + " extended.");
					break;
				}
				basiclog.warn("Error while extending Hit " + hitID
						+ ". Retrying in " + retryIn + " seconds...");
				try {
					Thread.sleep(1000 * retryIn);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
			}
			ResultOutputCollector.getInstance().addRequestedAssignments(1);
		} else {
			// we need to escalate the hit
			basiclog.info("ESCALATING " + hitID + "\n");

			ResultOutputCollector.getInstance().put(hitID, r);
			r.setCorrectSolution(null);
			r.setPhiCorrect(-2);
			ResultOutputCollector.getInstance().escalateHit(hitID);
			
			AbstractAssignmentAnswer correct = AssignmentAnswerFactory.createInstance("EscalatedHIT");
			// update worker statistics
			for (int i = 0; i < workers.length; i++) {
				((CSP1Capable)workers[i]).addAssignmentForBinomialCSP1(true, answers[i]
						.equals(correct));
			}
		}

	}
}
